﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Web;
using TimetableCSP.Scheduler;
using TimetableCSP.Scheduler.Entities;
using TimetableCSP.UI.Web.Entities;

namespace TimetableCSP.UI.Web
{
	public partial class TimetableGenerator : System.Web.UI.Page
	{
		private const string persistedTimetableKey = "PersistedTimetable";
		private string[] dayNames = new string[] {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
		private Classroom[] classroomsCache;
		private const string dateFormat = "yyyy-MM-dd HH:mm";
		//
		// The timeslots available on any given day
		//
		private static Timeslot[] timeslots = new Timeslot[] {
			new Timeslot() {Start = new TimeSpan(8, 30, 0), Stop = new TimeSpan(10,5,0)},
			new Timeslot() {Start = new TimeSpan(10, 20, 0), Stop = new TimeSpan(11,55,0)},
			new Timeslot() {Start = new TimeSpan(12, 20, 0), Stop = new TimeSpan(13,55,0)},
			new Timeslot() {Start = new TimeSpan(14, 0, 0), Stop = new TimeSpan(15,40,0)},
			new Timeslot() {Start = new TimeSpan(15, 45, 0), Stop = new TimeSpan(17,20,0)}
		};

		#region Properties
		/// <summary>
		/// Gets or sets the timetable that is stored in the user's session object.
		/// </summary>
		/// <value>
		/// The persisted timetable.
		/// </value>
		private IEnumerable<AbstractVariable> PersistedTimetable
		{
			get
			{
				return Session[persistedTimetableKey] as IEnumerable<AbstractVariable>;
			}
			set
			{
				Session[persistedTimetableKey] = value;
			}
		}

		/// <summary>
		/// Gets the classrooms.
		/// </summary>
		private Classroom[] Classrooms
		{
			get
			{
				if (this.classroomsCache == null)
				{
					this.classroomsCache = DataProvider.GetClassrooms().ToArray();
				}
				return this.classroomsCache;
			}
		}
		#endregion

		#region Event handlers
		protected void Page_Load(object sender, EventArgs e)
		{
			//
			// Prevent the browser from caching this page 
			//
			Response.Cache.SetCacheability(HttpCacheability.NoCache);
			Response.Cache.SetExpires(DateTime.Now.AddDays(-1));
			//
			// Inspect the querystring to figure out what data to return
			//
			string requestedData = Request.QueryString["Data"];
			string eventID = Request.QueryString["EventId"];
			switch (requestedData)
			{
				case "ViewOptions":
					ReturnViewOptions();
					break;
				case "Timeslots":
					ReturnTimeslots();
					break;
				case "GenerateNew":
					GenerateNewTimetable(false, null, -1);
					break;
				case "Pin":
					int newTimeslot = -1;
					if (!string.IsNullOrEmpty(Request.QueryString["Timeslot"]))
					{
						newTimeslot = int.Parse(Request.QueryString["Timeslot"]);
					}
					GenerateNewTimetable(true, eventID, newTimeslot);
					break;
				case "Unpin":
					Unpin(eventID);
					break;
				default:
					ReturnTimetable();
					break;
			}
			//
			// Suppress the default output of the HTML in the aspx file.
			//
			Response.End();
		}
		#endregion

		#region AJAX methods
		private void Unpin(string eventID)
		{
			//
			// Set the correct content-type
			//
			Response.ContentType = "application/json";
			//
			// Construct an object that holds information about the result of this operation
			//
			OperationResults result = new OperationResults();
			//
			// Find the class/event that should be unpinned
			//
			ClassVariable variableToUnpin = PersistedTimetable.First(x => x.Id.ToString() == eventID) as ClassVariable;
			if (variableToUnpin == null)
			{
				result.Success = false;
				result.Message = "No class with Id " + eventID + " could be found";
			}
			else
			{
				//
				// Unpin the class/event
				//
				variableToUnpin.Pinned = false;
				result.Success = true;
			}
			//
			// Serialize the 'result' object to JSON and write it out to the client
			//
			DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(OperationResults));
			serializer.WriteObject(Response.OutputStream, result);
		}

		private void ReturnTimeslots()
		{
			//
			// Set the correct content-type
			//
			Response.ContentType = "application/json";
			//
			// Serialize the timeslot array to JSON and write it out to the client
			//
			DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(Timeslot[]));
			serializer.WriteObject(Response.OutputStream, timeslots);
		}

		private void ReturnViewOptions()
		{
			//
			// Set the correct content-type
			//
			Response.ContentType = "application/json";
			//
			// Construct an object that contains a list of teachers, courses and classrooms
			//
			ViewOptions result = new ViewOptions();
			SortedDictionary<string, string> teachers = new SortedDictionary<string, string>();
			SortedDictionary<string, string> courses = new SortedDictionary<string, string>();
			foreach (ClassVariable classVariable in DataProvider.GetClasses())
			{
				teachers[classVariable.Teacher] = null;
				courses[classVariable.CourseId] = null;
			}
			result.Teachers = teachers.Keys.ToArray();
			result.Courses = courses.Keys.ToArray();
			result.Classrooms = this.Classrooms.Select(x => x.Name).ToArray();
			//
			// Serialize the resulting object to JSON and write it out to the client
			//
			DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(ViewOptions));
			serializer.WriteObject(Response.OutputStream, result);
		}

		private void GenerateNewTimetable(bool pin, string eventID, int newTimeslot)
		{
			//
			// Set the correct content-type
			//
			Response.ContentType = "application/json";
			//
			// Construct an array of the constraints we want to use
			//
			AbstractConstraint[] constraints = new AbstractConstraint[] 
			{ 
				new TeacherConstraint(), 
				new ClassroomConstraint(), 
				new GroupConstraint(),
				new ClassroomSizeConstraint( this.Classrooms ),
				new SameDayConstraint()
			};
			AbstractVariable[] classes;
			//
			// Should we pin a class/event?
			//
			if (pin)
			{
				//
				// Clone the current timetable 
				//
				List<AbstractVariable> clonedList = new List<AbstractVariable>();
				foreach (AbstractVariable variable in PersistedTimetable)
				{
					clonedList.Add((AbstractVariable)variable.Clone());
				}
				classes = clonedList.ToArray();
				//
				// Move the event/class to a new timeslot and pin it
				//
				ClassVariable pinnedVariable = classes.First(x => x.Id.ToString() == eventID) as ClassVariable;
				if (newTimeslot >= 0)
				{
					pinnedVariable[ClassVariable.TimeslotAttributeName] = newTimeslot;
				}
				pinnedVariable.Pinned = true;
			}
			else
			{
				//
				// We're generating a new timetable from scratch
				//
				classes = DataProvider.GetClasses().ToArray();
			}
			//
			// Set the domain ceilings for the class variables
			//
			int totalTimeslots = timeslots.Length * 5; // Five days
			foreach (ClassVariable classVariable in classes)
			{
				classVariable.SetDomainCeiling(ClassVariable.ClassroomAttributeName, this.Classrooms.Length);
				classVariable.SetDomainCeiling(ClassVariable.TimeslotAttributeName, totalTimeslots);
			}
			//
			// Construct an object that will hold information about the result of this operation
			//
			OperationResults result = new OperationResults();
			try
			{
				//
				// Try and find a solution to our timetabling problem using Min-conflicts.
				//
				List<Change> changes;
				IEnumerable<AbstractVariable> solution = MinConflicts.FindSolution(classes, constraints, 500, !pin, out changes);
				//
				// Was no solution found?
				//
				if (solution == null)
				{
					result.Success = false;
					if (pin)
					{
						result.Message = "The last pin operation will be undone since it caused an unsolvable conflict. You may have to unpin another class.";
					}
					else
					{
						result.Message = "Timetable generation failed. Try again.";
					}
				}
				else
				{
					//
					// Store this solution/timetable
					//
					PersistedTimetable = solution;
					result.Success = true;
					if (pin)
					{
						//
						// Include a message that explains the side-effects (if any) the move/pin operation had
						//
						result.Message = ConstructChangeMessage(changes);
					}
				}
			}
			catch (Exception ex)
			{
				result.Success = false;
				result.Message = ex.ToString();
			}
			//
			// Serialize the 'result' object to JSON and write it out to the client
			//
			DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(OperationResults));
			serializer.WriteObject(Response.OutputStream, result);
		}
		#endregion

		#region XML generation for the scheduler component
		private void ReturnTimetable()
		{
			//
			// Set the correct content-type
			//
			Response.ContentType = "text/xml";
			//
			// Write the starting tag
			//
			Response.Write("<data>");
			//
			// Get the timetable
			//
			IEnumerable<AbstractVariable> solution = PersistedTimetable;
			string filterValue = Request.QueryString["Filter"];
			bool showSummary = true; // Summary = Overview
			//
			// Filter the contents of the timetable if the user has selected a filter (by course, by teacher etc)
			//
			if (!string.IsNullOrEmpty(filterValue))
			{
				solution = FilterTimetable(solution, filterValue);
				showSummary = false;
			}
			if (solution != null)
			{
				if (showSummary)
				{
					//
					// Group all classes in the same timeslot into a single 'event' in the scheduler
					//
					foreach (IGrouping<int, AbstractVariable> classGroup in solution.GroupBy(x => ((ClassVariable)x).Timeslot))
					{
						Response.Write(ConvertToEventXml(classGroup));
					}
				}
				else
				{
					foreach (AbstractVariable variable in solution)
					{
						Response.Write(ConvertToEventXml((ClassVariable)variable, this.Classrooms));
					}
				}
			}
			//
			// Write the closing tag
			//
			Response.Write("</data>");
		}

		private string ConvertToEventXml(ClassVariable classVariable, Classroom[] classrooms)
		{
			string text = classVariable.CourseId + "<br/>" + classrooms[classVariable.Classroom].Name;
			string details = classVariable.Teacher + "<br/>" + classVariable.Name;
			return ConstructEventTag(classVariable.Id, classVariable.Timeslot, classVariable.Pinned, text, details);
		}

		private string ConvertToEventXml(IGrouping<int, AbstractVariable> classGroup)
		{
			int numberOfClasses = 0;
			string details = String.Empty;
			foreach (var abstractVariable in classGroup)
			{
				ClassVariable classVariable = (ClassVariable)abstractVariable;
				if (details.Length > 0)
				{
					details += "<br/>";
				}
				details += classVariable.CourseId + " - " + this.Classrooms[classVariable.Classroom].Name;
				numberOfClasses++;
			}
			return ConstructEventTag(classGroup.Key, classGroup.Key, false, string.Format("{0} classes", numberOfClasses), details);
		}

		private void FindStartAndStopTimes(int timeslot, DateTime startingPoint, out DateTime start, out DateTime stop)
		{
			//
			// Move the starting point to the correct day.
			// timeslots.Length gives us the number of timeslots in a single day.
			// Imagine that timeslot = 12 and timeslots.Length = 5, then the starting point will be moved forward by two days.
			//
			startingPoint = startingPoint.AddDays(timeslot / timeslots.Length);
			//
			//  Find the number of the timeslot within the day.
			//
			int slotIndex = timeslot % timeslots.Length;
			//
			// Construct the exact start and end date/time objects
			//
			start = startingPoint.Add(timeslots[slotIndex].Start);
			stop = startingPoint.Add(timeslots[slotIndex].Stop);
		}

		private string ConstructEventTag(int id, int timeslot, bool pinned, string text, string details)
		{
			DateTime monday = DateTime.Now.AddDays(1 - (int)DateTime.Now.DayOfWeek).Date;
			//
			// If today is Sunday, use last week's Monday
			//
			if (DateTime.Now.DayOfWeek == DayOfWeek.Sunday)
			{
				monday = monday.AddDays(-7);
			}
			DateTime start;
			DateTime stop;
			FindStartAndStopTimes(timeslot, monday, out start, out stop);
			return ConstructEventTag(id, start, stop, pinned, text, details);
		}

		private string ConstructEventTag(int id, DateTime start, DateTime stop, bool pinned, string text, string details)
		{
			return string.Format("<event id=\"{0}\" start_date=\"{1}\" end_date=\"{2}\" text=\"{3}\" details=\"{4}\" pinned=\"{5}\" />",
				id,
				start.ToString(dateFormat),
				stop.ToString(dateFormat),
				HttpUtility.HtmlEncode(text),
				HttpUtility.HtmlEncode(details),
				Convert.ToByte(pinned));
		}
		#endregion

		#region Utility/helper methods
		private IEnumerable<AbstractVariable> FilterTimetable(IEnumerable<AbstractVariable> solution, string filterValue)
		{
			//
			// The filterValue string should be of the form <key>:<value>
			//
			int colonPosition = filterValue.IndexOf(':');
			if (colonPosition == -1)
			{
				throw new ArgumentException("Filter value must contain ':' as a separator");
			}
			string key = filterValue.Substring(0, colonPosition);
			string value = filterValue.Substring(colonPosition + 1);
			//
			// Filter and return the list of classes using the appropriate predicate
			//
			switch (key)
			{
				case "teacher":
					return solution.Where(x => ((ClassVariable)x).Teacher == value);
				case "course":
					return solution.Where(x => ((ClassVariable)x).CourseId == value);
				case "classroom":
					int classroomIndex = FindClassroomIndex(value);
					return solution.Where(x => ((ClassVariable)x).Classroom == classroomIndex);
				default:
					throw new ArgumentException(string.Format("Unrecognized filter key '{0}'", key));
			}
		}

		private int FindClassroomIndex(string classroomName)
		{
			for (int index = 0; index < this.Classrooms.Length; index++)
			{
				if (this.Classrooms[index].Name == classroomName)
				{
					return index;
				}
			}
			return -1;
		}

		private string ConstructChangeMessage(List<Change> changes)
		{
			StringBuilder stringBuilder = new StringBuilder();
			stringBuilder.Append("The following changes have been made to resolve conflicts created by the last move:<ul>");
			bool noChanges = true;
			foreach (Change change in changes)
			{
				noChanges = false;
				ClassVariable classVariable = change.Variable as ClassVariable;
				stringBuilder.AppendFormat("<li>{0} {1} has been moved to ", classVariable.CourseId, classVariable.Name);
				if (change.Attribute == ClassVariable.ClassroomAttributeName)
				{
					//
					// Class was moved to another classroom
					//
					stringBuilder.Append(this.Classrooms[change.ModifiedValue].Name);
				}
				else
				{
					//
					// Class was moved to a different timeslot
					//
					stringBuilder.AppendFormat("{0:00}:{1:00} on {2}",
						timeslots[change.ModifiedValue % 5].Start.Hours,
						timeslots[change.ModifiedValue % 5].Start.Minutes,
						dayNames[(change.ModifiedValue / 5) + 1]);
				}
			}
			if (noChanges)
			{
				return null;
			}
			stringBuilder.Append("</ul>");
			return stringBuilder.ToString();
		}
		#endregion
	}
}